Formatting Conditional Compilation Directives
Fantomas is able to format code that contains conditional compiler directives.
In order to achieve this, Fantomas will actually format the code multiple times and merge all results afterwards.
Compilation directives and the syntax tree
The F# parser will construct a different syntax tree based on the provided compilation directives.
Consider the following piece of code:
let a =
#if DEBUG
0
#else
1
#endif
When parsing this code without any directives, the #else
branch will be considered the active code path.
The AST would be:
ImplFile
(ParsedImplFileInput
("tmp.fsx", true, QualifiedNameOfFile Tmp$fsx, [], [],
[SynModuleOrNamespace
([Tmp], false, AnonModule,
[Let
(false,
[SynBinding
(None, Normal, false, false, [],
PreXmlDoc ((1,0), FSharp.Compiler.Xml.XmlDocCollector),
SynValData
(None, SynValInfo ([], SynArgInfo ([], false, None)), None),
Named (SynIdent (a, None), false, None, tmp.fsx (1,4--1,5)),
None, Const (Int32 1, tmp.fsx (5,4--5,5)), tmp.fsx (1,4--1,5),
Yes tmp.fsx (1,0--5,5),
{ LetKeyword = Some tmp.fsx (1,0--1,3)
EqualsRange = Some tmp.fsx (1,6--1,7) })],
tmp.fsx (1,0--5,5))], PreXmlDocEmpty, [], None,
tmp.fsx (1,0--6,10), { ModuleKeyword = None
NamespaceKeyword = None })], (false, false),
{ ConditionalDirectives =
[If (Ident "DEBUG", tmp.fsx (2,4--2,13)); Else tmp.fsx (4,4--4,9);
EndIf tmp.fsx (6,4--6,10)]
CodeComments = [] }))
Notice that the right hand expression of binding a
is Const (Int32 1, ...)
.
There is no mention of 0
as that code was not active and thus is not a part of the syntax tree.
Passing [ "DEBUG" ]
to the parser will influence the lexer. The lexer will tokenize the other code branch and take the #if DEBUG
path this time.
Leading to
ImplFile
(ParsedImplFileInput
("tmp.fsx", true, QualifiedNameOfFile Tmp$fsx, [], [],
[SynModuleOrNamespace
([Tmp], false, AnonModule,
[Let
(false,
[SynBinding
(None, Normal, false, false, [],
PreXmlDoc ((1,0), FSharp.Compiler.Xml.XmlDocCollector),
SynValData
(None, SynValInfo ([], SynArgInfo ([], false, None)), None),
Named (SynIdent (a, None), false, None, tmp.fsx (1,4--1,5)),
None, Const (Int32 0, tmp.fsx (3,4--3,5)), tmp.fsx (1,4--1,5),
Yes tmp.fsx (1,0--3,5),
{ LetKeyword = Some tmp.fsx (1,0--1,3)
EqualsRange = Some tmp.fsx (1,6--1,7) })],
tmp.fsx (1,0--3,5))], PreXmlDocEmpty, [], None,
tmp.fsx (1,0--6,10), { ModuleKeyword = None
NamespaceKeyword = None })], (false, false),
{ ConditionalDirectives =
[If (Ident "DEBUG", tmp.fsx (2,4--2,13)); Else tmp.fsx (4,4--4,9);
EndIf tmp.fsx (6,4--6,10)]
CodeComments = [] }))
This tree is almost identical but the constant value is now Const (Int32 0, ...)
.
Multiple trees
As the combination of directives has an influence on the tree, Fantomas first parses the tree without any directives.
This base tree is then being inspected for ConditionalDirectiveTrivia.
We determine the different combinations in the Defines
module.
As trivia is being restored in each tree, they all will have gaps in them.
The first result will look like:
let a =
#if DEBUG
#else
1
#endif
and the second:
let a =
#if DEBUG
0
#else
#endif
Merging the trees
Once every tree is formatted, we chop each file into fragments.
A fragment is everything between a conditional directive #if | #else | #endif
or an actual directive.
This means fragments can also be empty strings.
Each result should have the same amount of fragments before we can merge them together.
If this is not the case, it means that somewhere a trivia was not properly restored.
If the number of fragments add up in each tree, then we merge two trees by reducing both lists and comparing each fragment.
We always take the longest fragment and thus picking the active code.
// fragments of []
[ "let a ="; "#if DEBUG"; ""; "#else"; "1"; "#endif" ]
// fragments of [ "DEBUG" ]
[ "let a ="; "#if DEBUG"; "0"; "#else"; ""; "#endif" ]
After merging:
[ "let a ="; "#if DEBUG"; "0"; "#else"; "1"; "#endif" ]